iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
Software Development

手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫系列 第 19

Day 19:Redis 持久化之 AOF (Append-Only File) 原理解析與基礎實作

  • 分享至 

  • xImage
  •  

記憶體資料庫最怕的就是突然停電,辛辛苦苦存的資料瞬間灰飛煙滅。為了解決這問題,Redis 提供了 AOF (Append-Only File) 日誌與 RDB (Redis Database) 快照兩種方案。

今天先實作 AOF 的基礎寫入,至少讓資料不只存在記憶體裡,重啟時還有機會救回來。


AOF 的運作原理

AOF 說穿了就是記帳:每次成功執行寫命令(像 SETDELLPUSH),就把那條命令用 RESP 格式追加到日誌檔尾端。

伺服器重啟時,再從頭到尾 replay 這些寫命令,把記憶體狀態重建回來。

客戶端寫請求 ----> [執行命令] ----> 寫入記憶體 DB
                         |
                         v
                    [追加 AOF] ----> 寫入 appendonly.aof 檔案

AOF 與 WAL (Write-Ahead Log) 的區別

在傳統的關係型資料庫(如 MySQL, PostgreSQL)中,通常使用 WAL (預寫式日誌) 機制——先寫日誌,再寫資料庫記憶體。這是為了保證崩潰恢復(Crash Recovery)的安全。

而 Redis 採用了相反的順序:先執行命令修改記憶體,再寫入 AOF 日誌。
這樣做有幾個好處:

  1. 避免無效檢查:如果命令語法有錯,執行時會直接報錯,就不會寫入 AOF,避免了日誌中混入無效命令。
  2. 不阻塞當前寫請求:命令先在記憶體中快速執行完畢回覆給客戶端,隨後才追加日誌,降低了請求延遲。

程式碼實作:AofLogger 基礎骨架

我在 code/db/aof.go 中實現了 AofLogger

1. 結構定義與開啟檔案

type AofLogger struct {
	mu       sync.Mutex
	file     *os.File
	filename string
	dbEngine *DB
	stopSync chan struct{}
}

func NewAofLogger(filename string, dbEngine *DB) (*AofLogger, error) {
	// 以 O_APPEND 追加模式開啟檔案,若不存在則建立
	file, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
	if err != nil {
		return nil, err
	}
	// ... 初始化結構 ...
}

2. 追加寫入命令

每當dispatcher 執行寫命令成功後,會呼叫此方法將其序列化並寫入檔案:

func (a *AofLogger) Write(val resp.Value) error {
	a.mu.Lock()
	defer a.mu.Unlock()

	bytes := val.Marshal() // 直接將命令的 RESP 表示寫入檔案
	_, err := a.file.Write(bytes)
	return err
}

3. 啟動加載與重播 (Load & Replay)

寫這個 AOF 載入時,我差點把游標搞丟,忘記把 a.file.Seek(0, io.SeekEnd) 加在最後,結果新命令直接覆蓋掉原本的檔案內容,找了半天才抓到這個雷。

當伺服器啟動時,我在 Start() 中調用 LoadAndReplay,利用之前實作的 resp.Reader 逐個讀取 RESP 命令並重新分發執行:

func (a *AofLogger) LoadAndReplay(replayFunc func(val resp.Value)) error {
	a.mu.Lock()
	defer a.mu.Unlock()

	// 定位到檔案開頭
	_, err := a.file.Seek(0, io.SeekStart)
	if err != nil { return err }

	reader := resp.NewReader(a.file)
	for {
		val, err := reader.ReadValue()
		if err != nil {
			if errors.Is(err, io.EOF) { break } // 讀到檔案結尾,正常退出
			return err
		}
		replayFunc(val) // 呼叫重播回呼,在記憶體中重跑命令
	}

	// 重播完成後,指針定位回結尾以利追加
	_, err = a.file.Seek(0, io.SeekEnd)
	return err
}

跑起來看看

先把 Server 跑起來(記得開啟 AOF 設定):

go run ./code/main.go

我們開另一個終端機,隨便寫入幾筆資料:

printf "*3\r\n\$3\r\nSET\r\n\$4\r\nname\r\n\$5\r\nalice\r\n" | nc localhost 6379
printf "*3\r\n\$3\r\nSET\r\n\$3\r\nage\r\n\$2\r\n18\r\n" | nc localhost 6379

然後直接去檢查專案目錄底下長出來的 appendonly.aof 檔案:

cat appendonly.aof

# 預期輸出(RESP 格式的指令日誌):
# *3
# $3
# SET
# $4
# name
# $5
# alice
# *3
# $3
# SET
# $3
# age
# $2
# 18

總結

今天把 AOF 的基本追加和啟動 replay 先弄起來。實作這個的時候一開始用字串拼接硬幹,結果壓測直接 OOM 被系統砍掉,改成 bufio.Writer 配 buffer 之後才算勉強撐住。

不過現在每次寫指令都會碰磁碟,效能肯定不好看。明天來補每秒非同步 fsync,看看 I/O 能不能救回來一點。


上一篇
Day 18:支援 Pipeline 批次命令處理與 TCP 讀取效能優化
下一篇
Day 20:實作 AOF 的每秒非同步 fsync(everysec)機制
系列文
手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言